<?php
/**
 * Leek - Zend Framework Components
 *
 * @category   Leek
 * @package    Leek_Acl
 * @author     Chris Jones <leeked@gmail.com>
 * @link       http://code.google.com/p/leek-zf-components/
 * @license    http://www.opensource.org/licenses/bsd-license.php New BSD License
 * @version    $Id: Acl.php 80 2010-01-07 16:15:56Z leeked $
 */

/**
 * File based ACL
 *
 * @uses       Zend_Acl
 * @category   Leek
 * @package    Leek_Acl
 * @author     Chris Jones <leeked@gmail.com>
 * @link       http://code.google.com/p/leek-zf-components/
 * @license    http://www.opensource.org/licenses/bsd-license.php New BSD License
 */
class Leek_Acl extends Zend_Acl
{
    /**
     * Singleton instance
     * @var Zend_Acl
     */
    protected static $_instance = null;

    /**
     * Resources directory
     * @var array
     */
    protected $_resourcesDirectories = array();

    /**
     * Default Role Name
     * @var mixed
     */
    protected $_defaultRole;

    /**
     * Zend_Auth role field name
     * @var string
     */
    protected $_authRoleField;

    /**
     * Resources files type
     * @var string
     */
    protected $_resourceFilesType = 'php';

    /**
     * Resource tree (extension)
     *
     * @var array
     */
    protected $_resourcesExt = array();

    /**
     * Options for when a user gets denied access
     * @var array
     */
    private $_denyRedirectDefaultOptions = array(
        'module'     => null,
        'controller' => null,
        'action'     => null,
        'message'    => null,
        'type'       => null,
    );

    /**
     * Options for when a user is not logged in
     * @var array
     */
    private $_noAuthRedirectDefaultOptions = array(
        'module'     => null,
        'controller' => null,
        'action'     => null,
        'message'    => null,
        'type'       => null,
    );

    /**
     * Sets a deny default option
     *
     * @param string $key
     * @param string $value
     * @return Leek_Acl
     */
    public function setDenyRedirectDefaultOption($key, $value)
    {
        if (array_key_exists($key, $this->_denyRedirectDefaultOptions)) {
            $this->_denyRedirectDefaultOptions[$key] = $value;
        }

        return $this;
    }

    /**
     * Sets a no auth default option
     *
     * @param string $key
     * @param string $value
     * @return Leek_Acl
     */
    public function setNoAuthRedirectDefaultOption($key, $value)
    {
        if (array_key_exists($key, $this->_noAuthRedirectDefaultOptions)) {
            $this->_noAuthRedirectDefaultOptions[$key] = $value;
        }

        return $this;
    }

    /**
     * Return deny options for a resource
     *
     * @param string $resourceName
     * @return array
     */
    public function getDenyRedirectOptions($resourceName)
    {
        $options = isset($this->_resourcesExt[$resourceName]['denyredirect']) ? $this->_resourcesExt[$resourceName]['denyredirect'] : array();
        return array_merge($this->_denyRedirectDefaultOptions, $options);
    }

    /**
     * Return no auth options for a resource
     *
     * @param string $resourceName
     * @return array
     */
    public function getNoAuthRedirectOptions($resourceName)
    {
        $options = isset($this->_resourcesExt[$resourceName]['noauthredirect']) ? $this->_resourcesExt[$resourceName]['noauthredirect'] : array();
        return array_merge($this->_noAuthRedirectDefaultOptions, $options);
    }

    /**
     * Singleton pattern implementation makes "new" unavailable
     *
     * @return void
     */
    protected function __construct()
    {}

    /**
     * Singleton pattern implementation makes "clone" unavailable
     *
     * @return void
     */
    protected function __clone()
    {}

    /**
     * Returns an instance of Leek_Acl
     *
     * Singleton pattern implementation
     *
     * @return Leek_Acl Provides a fluent interface
     */
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }

        return self::$_instance;
    }

    /**
     * Sets the current resource files filetype
     *
     * @param string $type
     * @return Leek_Controller_Router_Rewrite
     */
    public function setResourceFilesType($type)
    {
        $this->_resourceFilesType = $type;
        return $this;
    }

    /**
     * Returns the current resource files filetype
     *
     * @return string
     */
    public function getResourceFilesType()
    {
        return $this->_resourceFilesType;
    }

    /**
     * Set the default role name
     *
     * @return Leek_Acl
     */
    public function setDefaultRole($role)
    {
        $this->_defaultRole = $role;
        return $this;
    }

    /**
     * Return the default role name
     *
     * @return string|null
     */
    public function getDefaultRole()
    {
        return $this->_defaultRole;
    }

    /**
     * Set the Zend_Auth::getIdentity() role field name
     *
     * @return Leek_Acl
     */
    public function setAuthRoleField($fieldName)
    {
        $this->_authRoleField = (string) $fieldName;
        return $this;
    }

    /**
     * Return the Zend_Auth::getIdentity() role field name
     *
     * @return string
     */
    public function getAuthRoleField()
    {
        return $this->_authRoleField;
    }

    /**
     * Sets the roles filename
     *
     * @param string $rolesFileName
     * @return Leek_Acl
     */
    public function setRolesFileName($rolesFileName)
    {
        $this->_rolesFileName = (string) $rolesFileName;
        return $this;
    }

    /**
     * Gets the roles filename
     *
     * @return string
     */
    public function getRolesFileName()
    {
        return $this->_rolesFileName;
    }

    /**
     * Add roles
     *
     * @param array $roles
     * @return Leek_Acl
     */
    public function addRoles(array $roles)
    {
        foreach ($roles as $roleName => $roleOptions) {

            if (!$this->hasRole($roleName)) {

                $roleOptions = array_change_key_case($roleOptions, CASE_LOWER);

                if (isset($roleOptions['extends'])) {
                    $this->addRole(new Zend_Acl_Role($roleName), $roleOptions['extends']);
                } else {
                    $this->addRole(new Zend_Acl_Role($roleName));
                }

                if (isset($roleOptions['default']) && $roleOptions['default']) {
                    $this->setDefaultRole($roleName);
                }

            }

        }

        return $this;
    }

    /**
     * Adds a path to the stack of resource directories
     *
     * @param string $path
     * @param string|null $key
     * @return Leek_Acl
     */
    public function addResourcesDirectory($path, $key = null)
    {
        $path = rtrim((string) $path, '/\\');

        if ($key === null) {
            $this->_resourcesDirectories[] = $path;
        } else {
            $this->_resourcesDirectories[$key] = $path;
        }

        return $this;
    }

    /**
     * Returns all registered paths for resources directories
     *
     * @return array
     */
    public function getResourcesDirectories()
    {
        return $this->_resourcesDirectories;
    }

    /**
     * Add resources from file
     *
     * @param string $controller
     * @param string|null $module
     * @return Leek_Acl
     */
    public function addResourcesFromFile($controller, $module = null)
    {
        $resourcePaths = $this->getResourcesDirectories();
        $resourceFile  = $controller . '.' . $this->getResourceFilesType();

        if ($module !== null) {

            // Only load from the specified module
            if (isset($resourcePaths[$module])) {
                $resourcePaths = array(
                    $module => $resourcePaths[$module]
                );
            } else {
                $resourcePaths = array();
            }

        }
 
        foreach ($resourcePaths as $resourceKey => $resourcePath) {

            $file = $resourcePath . DIRECTORY_SEPARATOR . $resourceFile;

            // If the file doesn't exist, don't load any resources
            if (file_exists($file)) {
                $resources = Leek_Config::loadConfig($file);
                if (empty($module)) {
                    $this->addResources($resources);
                } else {
                    $this->addResources($resources, ('module='. $module));
                }
            }

        }

        return $this;
    }

    /**
     * Generates a resource name from a config directive.
     *
     * @param array $resource
     * @param string $type Module, Controller, Action
     * @param string $extends
     * @return string
     */
    private function _createNameFromResource(array $resource, $type, $extends = null)
    {
        if (!isset($resource['name'])) {
            throw new Exception('Resource (' . $type . ') does not have a name.');
        }

        $extends    = !empty($extends) ? $extends . '&' : '';
        $module     = '';
        $controller = '';
        $action     = '';

        switch ($type) {

            case 'module':
                $module = $resource['name'];
                break;

            case 'controller':
                $controller = $resource['name'];
                if (isset($resource['module'])) {
                    $module = $resource['module'];
                }
                break;

            case 'action':
                $action = $resource['name'];
                if (isset($resource['controller'])) {
                    $module = $resource['controller'];
                }
                if (isset($resource['module'])) {
                    $module = $resource['module'];
                }
                break;

        }

        return $extends . $this->getResourceName($action, $controller, $module);
    }

    /**
     * Generates a resource name from a module, controller, and/or action.
     *
     * @param string $action
     * @param string $controller
     * @param string $module Optional
     * @return string
     */
    public function getResourceName($action, $controller, $module = null)
    {
        $data = array();

        if (!empty($module)) {
            $data['module']     = $module;
        }

        if (!empty($controller)) {
            $data['controller'] = $controller;
        }

        if (!empty($action)) {
            $data['action']     = $action;
        }

        return http_build_query($data);
    }

    /**
     * Adds a resource
     *
     * @param array $resource
     * @param string $type
     * @param string|null $extends Optional
     * @return Leek_Acl
     */
    public function createAddResource(array $resource, $type, $extends = null)
    {
        $name = $this->_createNameFromResource($resource, $type, $extends);

        // Add Resource
        if (!$this->has($name)) {
            if (empty($extends)) {
                $this->addResource(new Zend_Acl_Resource($name));
            } else {
                if (!$this->has($extends)) {
                    $this->addResource(new Zend_Acl_Resource($extends));
                }
                $this->addResource(new Zend_Acl_Resource($name), $extends);
            }
        }

        // Allow/Deny
        foreach (array('allow', 'deny') as $type) {
            if (isset($resource[$type])) {
                $typeRoles = $resource[$type];
                if (is_string($typeRoles)) {
                    $this->$type($typeRoles, $name);
                } elseif (is_array($typeRoles)) {
                    foreach ($typeRoles as $role => $roleValue) {
                        if (is_integer($role)) {
                            $this->$type($roleValue, $name);
                        } else {
                            $this->$type($role, $name);
                        }
                    }
                }
            }
        }

        $resource = array_change_key_case($resource, CASE_LOWER);

        // Save any deny redirect options
        if (isset($resource['denyredirect'])) {
            $this->_resourcesExt[$name]['denyredirect'] = $resource['denyredirect'];
        }

        // Save any no auth redirect options
        if (isset($resource['noauthredirect'])) {
            $this->_resourcesExt[$name]['noauthredirect'] = $resource['noauthredirect'];
        }

        if (isset($resource['resources'])) {
            $this->addResources($resource['resources'], $name);
        }

        return $this;
    }

    /**
     * Adds an array of resources
     *
     * @param array $resources
     * @param string|null $extends Optional
     * @return Leek_Acl
     */
    public function addResources(array $resources, $extends = null)
    {
        if (isset($resources['resources'])) {
            $resources = $resources['resources'];
        }

        foreach (array('module', 'controller', 'action') as $type) {

            if (isset($resources[$type]) && !is_string($resources[$type])) {

                foreach ($resources[$type] as $resourceKey => $resourceValue) {

                    // Whether or not to break this iteration
                    $break    = false;
                    $resource = null;

                    if (is_integer($resourceKey)) {
                        $resource = array_change_key_case($resourceValue, CASE_LOWER);
                    } else {
                        $resource = $resources[$type];
                        $break    = true;
                    }

                    $this->createAddResource($resource, $type, $extends);

                    if ($break) {
                        break;
                    }

                }

            }

        }

        return $this;
    }

    /**
     * Validate a resource with a role
     *
     * @param string $resource
     * @param string|null $role
     * @return bool
     */
    public function validate($resource, $role = null)
    {
        // Pull current Role
        if (empty($role)) {

            $roleField = $this->getAuthRoleField();

            if (empty($roleField)) {

                // Use default
                $role = $this->getDefaultRole();

            } else {

                // Use Zend_Auth
                $auth = Zend_Auth::getInstance();
                $role = $auth->hasIdentity() ? $auth->getIdentity()->$roleField : $this->getDefaultRole();

            }

        }

        // Check if we have a resource for this
        if (!$this->has($resource)) {

            $resourceParts = array();
            parse_str($resource, $resourceParts);
            $resourceModule     = isset($resourceParts['module'])     ? 'module='     . $resourceParts['module']     : null;
            $resourceController = isset($resourceParts['controller']) ? 'controller=' . $resourceParts['controller'] : null;
            $resourceAction     = isset($resourceParts['action'])     ? 'action='     . $resourceParts['action']     : null;

            if (!empty($resourceAction)) {

                $extendModule     = $resourceModule;
                $extendController = empty($resourceModule) ? $resourceController : $resourceModule . '&' . $resourceController;

                if ($this->has($extendModule)) {
                    if (!$this->has($extendController)) {
                        $this->add(new Zend_Acl_Resource($extendController), $extendModule);
                    }
                }

                if ($this->has($extendController)) {
                    $this->add(new Zend_Acl_Resource($resource), $extendController);
                }
            }

        }

        if ($this->has($resource)) {

            // Check is a valid role
            if (!$this->hasRole($role)) {
                throw new Zend_Acl_Exception("Role '" . $role . "' is not a registered role.");
            }

            if (!$this->isAllowed($role, $resource)) {
                return false;
            }

        }

        // Passed ACL
        return true;
    }

}
